﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace NuIoc
{
    public sealed class Container : IContainer
    {
        public IEnumerable<object> GetInstances()
        {
            return _types
                .SelectMany(t => t.Instances)
                .Select(p => p.Value).ToArray();
        }

        public IEnumerable<object> GetInstances(Type type)
        {
            EnsureRegistered(type);

            return _types
                .First(t => t.ExposedType == type)
                .Instances
                .Select(p => p.Value).ToArray();
        }

        public IEnumerable<T> GetInstances<T>()
        {
            return GetInstances(typeof(T)).Cast<T>().ToArray();
        }

        public void Register(Type type, DependencyLifetime dependencyLifetime = DependencyLifetime.Persistent)
        {
            _Register(type, dependencyLifetime);
        }

        public void Register(Type type, Func<object> factory, DependencyLifetime dependencyLifetime = DependencyLifetime.Persistent)
        {
            _Register(type, factory, dependencyLifetime);
        }

        public void Register(Type abstractType, Type concreteType, DependencyLifetime dependencyLifetime = DependencyLifetime.Persistent)
        {
            _Register(abstractType, concreteType, dependencyLifetime);
        }

        public void Register<T>(DependencyLifetime dependencyLifetime = DependencyLifetime.Persistent) where T : class
        {
            Register(typeof(T), dependencyLifetime);
        }

        public void Register<T>(Func<T> factory, DependencyLifetime dependencyLifetime = DependencyLifetime.Persistent) where T : class
        {
            Register(typeof(T), factory, dependencyLifetime);
        }

        public void Register<TAbstract, TConcrete>(DependencyLifetime dependencyLifetime = DependencyLifetime.Persistent) where TConcrete : class, TAbstract
        {
            Register(typeof(TAbstract), typeof(TConcrete), dependencyLifetime);
        }

        public object Resolve(Type type)
        {
            EnsureRegistered(type);

            return _types
              .First(t => t.ExposedType == type)
              .Resolve(this);
        }

        public T Resolve<T>()
        {
            return (T)Resolve(typeof(T));
        }

        public void Unregister(Type type)
        {
            EnsureRegistered(type);

            var result = _types
                .First(t => t.ExposedType == type);
            _types.Remove(result);
        }

        public void Unregister<T>() where T : class
        {
            Unregister(typeof(T));
        }

        public bool IsRegistered(Type type)
        {
            return _types
                .Any(t => t.ExposedType == type);
        }

        public bool IsRegistered<T>()
        {
            return IsRegistered(typeof(T));
        }

        #region Internals 

        private List<InjectedType> _types = new List<InjectedType>();

        private void _Register(Type exposedType, DependencyLifetime dependencyLifetime = DependencyLifetime.Persistent)
        {
            EnsureNotRegistered(exposedType);

            var type = new InjectedType(exposedType, exposedType, dependencyLifetime);
            _types.Add(type);
            type.Prepare();
        }

        private void _Register(Type exposedType, Type actualType, DependencyLifetime dependencyLifetime = DependencyLifetime.Persistent)
        {
            EnsureNotRegistered(exposedType);

            var type = new InjectedType(exposedType, actualType, dependencyLifetime);
            _types.Add(type);
            type.Prepare();
        }

        private void EnsureRegistered(Type type)
        {
            if (!IsRegistered(type))
            {
                throw new InvalidOperationException("Type has not been registered.");
            }
        }

        private void EnsureNotRegistered(Type type)
        {
            if (IsRegistered(type))
            {
                throw new InvalidOperationException("Type has already been registered.");
            }
        }

        private void _Register(Type exposedType, Func<object> factory, DependencyLifetime dependencyLifetime = DependencyLifetime.Persistent)
        {
            EnsureNotRegistered(exposedType);

            var type = new InjectedType(exposedType, factory, dependencyLifetime);
            _types.Add(type);
            type.Prepare();
        }

        #endregion
    }

    class InjectedType
    {
        private Type _exposedType;
        private Type _actualType;

        private Func<object> _factory;

        private InjectedConstructor _constructor;
        private IList<InjectedProperty> _properties = new List<InjectedProperty>();
        private IList<InjectedMethod> _methods = new List<InjectedMethod>();

        private IDictionary<string, object> _instances = new Dictionary<string, object>();
        private DependencyLifetime _dependencyLifetime;

        public InjectedType(Type exposedType, DependencyLifetime dependencyLifetime)
        {
            _exposedType = exposedType;
            _dependencyLifetime = dependencyLifetime;
        }

        public InjectedType(Type exposedType, Type actualType, DependencyLifetime dependencyLifetime)
        {
            _exposedType = exposedType;
            _actualType = actualType;
            _dependencyLifetime = dependencyLifetime;
        }

        public InjectedType(Type exposedType, Func<object> factory, DependencyLifetime dependencyLifetime)
        {
            _exposedType = exposedType;
            _factory = factory;
            _dependencyLifetime = dependencyLifetime;
        }

        public Type ExposedType
        {
            get
            {
                return _exposedType;
            }
        }

        public Type ActualType
        {
            get
            {
                return _actualType;
            }
        }

        public DependencyLifetime DependencyLifetime
        {
            get
            {
                return _dependencyLifetime;
            }
        }

        public Func<object> Factory
        {
            get
            {
                return _factory;
            }
        }

        public InjectedConstructor Constructor
        {
            get
            {
                return _constructor;
            }
        }

        public IEnumerable<InjectedProperty> Properties
        {
            get
            {
                return new ReadOnlyCollection<InjectedProperty>(_properties);
            }
        }

        public IEnumerable<InjectedMethod> Methods
        {
            get
            {
                return new ReadOnlyCollection<InjectedMethod>(_methods);
            }
        }

        public IDictionary<string, object> Instances
        {
            get
            {
                return _instances;
            }
        }

        public void Prepare()
        {
            if (Factory == null)
            {
                // Get constructor
                var constructorInfo = _actualType.GetTypeInfo().DeclaredConstructors
                    .FirstOrDefault(p => HasValidInjectAttribute(p));
                if (constructorInfo == null)
                {
                    constructorInfo = _actualType.GetTypeInfo().DeclaredConstructors.First();
                }
                _constructor = new InjectedConstructor(this, constructorInfo);

                // Properties
                var properties = _actualType.GetRuntimeProperties()
                    .Where(p => HasValidInjectAttribute(p));
                foreach (var propertyInfo in properties)
                {
                    var p = new InjectedProperty(this, propertyInfo);
                    _properties.Add(p);
                }

                // Methods
                var methods = _actualType.GetRuntimeMethods()
                    .Where(p => HasValidInjectAttribute(p));
                foreach (var methodInfo in methods)
                {
                    var m = new InjectedMethod(this, methodInfo);
                    _methods.Add(m);
                }
            }
        }

        private static bool HasValidInjectAttribute(MemberInfo p)
        {
            return p.GetCustomAttributes(true).Any(a => {
                var type = a.GetType();
                return type.Name == "InjectAttribute";
            });
        }

        public object Resolve(IContainer container)
        {
            object instance = null;
            switch (DependencyLifetime)
            {
                case DependencyLifetime.Transient:
                    instance = New(container);
                    break;

                case DependencyLifetime.Persistent:
                    if (_instances.Any())
                    {
                        instance = _instances[string.Empty];
                    }
                    else
                    {
                        instance = New(container);
                        _instances[string.Empty] = instance;
                    }
                    break;
                    
            }
      
            return instance;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="container"></param>
        /// <returns></returns>
        private object New(IContainer container)
        {
            object instance = null;

            if (Factory == null)
            {
                instance = Constructor.ResolveAndCreate(container);
            }
            else
            {
                instance = Factory();
            }

            foreach (var property in Properties)
            {
                property.ResolveForInstance(container, instance);
            }

            foreach (var method in Methods)
            {
                method.ResolveAndInvokeForInstance(container, instance);
            }

            return instance;
        }
    }

    [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class InjectAttribute : Attribute
    {

    }

    class InjectedConstructor
    {
        private InjectedType _type;
        private ConstructorInfo _constructorInfo;

        public InjectedConstructor(InjectedType type, ConstructorInfo constructorInfo)
        {
            _type = type;
            _constructorInfo = constructorInfo;
        }

        public string Name
        {
            get
            {
                return _constructorInfo.Name;
            }
        }

        public InjectedType DeclaringType
        {
            get
            {
                return _type;
            }
        }

        public object ResolveAndCreate(IContainer container)
        {
            List<object> parameters = new List<object>();

            foreach (var parameterInfo in _constructorInfo.GetParameters())
            {
                var parameterType = parameterInfo.ParameterType;
                var arg = container.Resolve(parameterType);
                parameters.Add(arg);
            }

            var instance = _constructorInfo.Invoke(parameters.ToArray());

            return instance;
        }
    }

    class InjectedMethod
    {
        private InjectedType _type;
        private MethodInfo _methodInfo;

        public InjectedMethod(InjectedType type, MethodInfo methodInfo)
        {
            _type = type;
            _methodInfo = methodInfo;
        }

        public string Name
        {
            get
            {
                return _methodInfo.Name;
            }
        }

        public InjectedType DeclaringType
        {
            get
            {
                return _type;
            }
        }

        public void ResolveAndInvokeForInstance(IContainer container, object instance)
        {
            List<object> parameters = new List<object>();

            foreach (var parameterInfo in _methodInfo.GetParameters())
            {
                var parameterType = parameterInfo.ParameterType;
                var arg = container.Resolve(parameterType);
                parameters.Add(arg);
            }

            _methodInfo.Invoke(instance, parameters.ToArray());
        }
    }

    class InjectedProperty
    {
        private InjectedType _type;
        private PropertyInfo _propertyInfo;

        public InjectedProperty(InjectedType type, PropertyInfo propertyInfo)
        {
            _type = type;
            _propertyInfo = propertyInfo;
        }

        public string Name
        {
            get
            {
                return _propertyInfo.Name;
            }
        }

        public Type PropertyType
        {
            get
            {
                return _propertyInfo.PropertyType;
            }
        }

        public InjectedType DeclaringType
        {
            get
            {
                return _type;
            }
        }

        public void ResolveForInstance(IContainer container, object instance)
        {
            var propertyType = _propertyInfo.PropertyType;
            var arg = container.Resolve(propertyType);

            _propertyInfo.SetValue(instance, arg);
        }
    }
}
